/*
 * Copyright 2011 James Talmage
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package com.googlecode.genericsutils;

import java.lang.reflect.*;

import static com.googlecode.genericsutils.GenericTypeUtils.getGenericTypeArguments;

/**
 * User: jamestalmage
 * Date: 6/12/11
 * Time: 7:38 PM
 */
public class AssignableUtils {

    static Class getReflectionInterface(Type t){
        if(ParameterizedType.class.isInstance(t)) return ParameterizedType.class;
        if(TypeVariable.class.isInstance(t)) return TypeVariable.class;
        if(GenericArrayType.class.isInstance(t)) return GenericArrayType.class;
        if(WildcardType.class.isInstance(t)) return WildcardType.class;
        if(Class.class.isInstance(t)) return Class.class;
        throw new RuntimeException("WE'VE COVERED ALL REFLECTION TYPES - THIS SHOULDN'T HAPPEN");
    }

    public static boolean anyAssignable(Type assignedTo, Type ... attemptedValues){
        for (Type attemptedValue : attemptedValues) {
            if(isAssignable(assignedTo,attemptedValue)) return true;
        }
        return false;
    }

    //Would love to switch order here and use VarArgs... Seems dangerous to mix up the order on just this method though;
    public static boolean meetsAllBounds(Type[] assignedToBounds, Type attemptedValue){
        for (Type assignedToBound : assignedToBounds) {
            if(!isAssignable(assignedToBound,attemptedValue)) return false;
        }
        return true;
    }

    public static boolean isAssignable(Type assignedTo, Type attemptedValue) {
        Class assToClass = getReflectionInterface(assignedTo);
        Class attValClass = getReflectionInterface(attemptedValue);
        try {
            Method method = AssignableUtils.class.getDeclaredMethod("_isAssignable", assToClass, attValClass);
            return (Boolean)method.invoke(null,assignedTo,attemptedValue);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    static boolean assignablePairs(Type[] assignedTo, Type[] attemptedValue){
        if(assignedTo.length != attemptedValue.length) return false;
        for (int i = 0; i < assignedTo.length; i++){
            System.out.print("comparing: " + assignedTo[i] + " to " + attemptedValue[i]);
            if(!isAssignable(assignedTo[i],attemptedValue[i])){
                System.out.println(": failed");
                return false;
            }
            System.out.println(": passed");

        }
        return true;
    }

    static boolean _isAssignable (Class assignedTo, Class attemptedValue){
        return assignedTo.isAssignableFrom(attemptedValue);
    }

    static boolean _isAssignable (Class assignedTo, ParameterizedType attemptedValue){
        return isAssignable(assignedTo,attemptedValue.getRawType());
    }

    static boolean _isAssignable(Class assignedTo, TypeVariable attemptedValue){
        return anyAssignable(assignedTo,attemptedValue.getBounds());
    }

    static boolean _isAssignable(Class assignedTo, WildcardType wildcardType){
        return anyAssignable(assignedTo,wildcardType.getUpperBounds());
    }

    static boolean _isAssignable(ParameterizedType assignedTo, Class attemptedValue){
        if(!isAssignable(assignedTo.getRawType(),attemptedValue)) return false;
        Type[] attemptedArguments = getGenericTypeArguments(GenericTypeUtils.getClass(assignedTo), attemptedValue);
        return assignablePairs(assignedTo.getActualTypeArguments(),attemptedArguments);
    }

    static boolean _isAssignable (ParameterizedType assignedTo, ParameterizedType attemptedValue){
        if(!isAssignable(assignedTo.getRawType(), attemptedValue.getRawType())) return false;
        Type[] attemptedArguments = getGenericTypeArguments(GenericTypeUtils.getClass(assignedTo), attemptedValue);
        return assignablePairs(assignedTo.getActualTypeArguments(), attemptedArguments);
    }

    static boolean _isAssignable(ParameterizedType assignedTo, TypeVariable attemptedValue){
        return anyAssignable(assignedTo, attemptedValue.getBounds());
    }

    static boolean _isAssignable(ParameterizedType assignedTo, WildcardType  attemptedValue){
        return anyAssignable(assignedTo, attemptedValue.getUpperBounds());
    }

    static boolean _isAssignable (TypeVariable assignedTo, Class attemptedValue){
        return meetsAllBounds(assignedTo.getBounds(),attemptedValue);
    }

    static boolean _isAssignable (TypeVariable assignedTo, ParameterizedType attemptedValue){
        return meetsAllBounds(assignedTo.getBounds(),attemptedValue);
    }

    static boolean _isAssignable (TypeVariable assignedTo, TypeVariable attemptedValue){
        return meetsAllBounds(assignedTo.getBounds(),attemptedValue);
    }

    static boolean _isAssignable (TypeVariable assignedTo, WildcardType attemptedValue){
        return meetsAllBounds(assignedTo.getBounds(),attemptedValue);
    }

    static boolean _doWildcard(WildcardType assignedTo, Type attemptedValue){
        for(Type t: assignedTo.getLowerBounds()){
            if(!isAssignable(attemptedValue,t)) return false;
        }
        return meetsAllBounds(assignedTo.getUpperBounds(),attemptedValue);
    }

    static boolean _isAssignable (WildcardType assignedTo, Class attemptedValue){
        return _doWildcard(assignedTo, attemptedValue);
    }

    static boolean _isAssignable (WildcardType assignedTo, ParameterizedType attemptedValue){
        return _doWildcard(assignedTo, attemptedValue);
    }

    static boolean _isAssignable (WildcardType assignedTo, TypeVariable attemptedValue){
        return _doWildcard(assignedTo, attemptedValue);
    }

    static boolean _isAssignable (WildcardType assignedTo, WildcardType attemptedValue){
        return _doWildcard(assignedTo, attemptedValue);
    }

}
